一開始認識 reactive()
跟 ref()
真的超困惑,不理解兩者背後的差別,只能硬記個別的使用方式和限制,一直到了解他們背的原理,才有「啊哈~」的感覺,一切都變得合理起來。
所以這個主題會先從原生 Javascript 開始談起,再來連結到 reactive()
或 ref()
的特性與限制。
Object.defineProperty()
reactive()
響應原理ref()
響應原理註:reactive()
或 ref()
的特性與限制會在下一篇討論。
「響應式」指的是 Vue 幫助你即時更新相依資料這件事。
要做到這件事,其中一個要解決的問題就是,「要怎麼知道資料被改動了?」,Vue 3 推出的 reactive()
跟 ref()
就是用來攔截資料的讀取跟寫入,所以在開發上所有需要響應性的資料,都需要傳入 reactive()
或 ref()
來處理。
reactive()
是用 Proxy 來實作,ref()
則是用 get
和set
關鍵字來實作。
所以在進入 reactive()
或 ref()
之前,先來點原生 Javascript。
每次存取物件屬性,實際上會呼叫取值器(getter)。
每次對物件屬性做賦值,實際上會呼叫設值器(setter)。
原生 Javascript 可以透過物件實字或 Object.defineProperty
,搭配 get
和 set
關鍵字,重新去定義特定屬性的 getter 和 setter,從而改變讀取特定屬性或對特定屬性賦值時的行為。
以下示範的是透過物件實字建立:
const obj = {
value: "hello",
//每次讀取 obj.message 時會呼叫
get message1() {
console.log("有人用了物件的 getter 讀取 message1");
return this.value;
},
//每次對 obj.message 重新賦值會呼叫
set message1(newValue) {
console.log(`有人將物件的 message1 重新賦值為 ${newValue}`);
this.value = newValue;
},
};
let foo = obj.message1;
//印出 有人用了物件的 getter 讀取 message
obj.message1 = "你好";
//印出 有人將物件的 message 重新賦值為 你好
針對已經建立的物件,直接新增屬性是沒有用的,新增當下並沒有定義好 getter 和 setter:
obj.newProperty = "新增屬性";
想要新增屬性並自定義 getter 和 setter,需要透過 Object.defineProperty
:
Object.defineProperty(obj, property, descriptor)
延續使用前面實字建立的物件,針對該物件新增屬性和描述(getter & setter)
let value2 = "everyone";
Object.defineProperty(obj, "message2", {
get: function () {
console.log("有人用了物件的 getter 讀取 message2");
return value2;
},
set: function (newValue) {
console.log(`有人將物件的 message2 重新賦值為 ${newValue}`);
value2 = newValue;
},
});
obj.message2 = "Hey hey hey";
// 呼叫 setter
// 印出 有人將物件的 message 重新賦值為 Hey hey hey
缺點:
註:在 Vue 2 的時候,是透過 Object.defineProperty
來實作響應性。
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Vue 2 - 深入响应式原理
基於 Object.defineProperty
的限制,Vue 2 對於響應式物件和陣列的操作有一些的限制,好奇的人可以再去了解,這裡就不細說。
Proxy 是原生 Javascript 的語法,翻譯為「代理」,只要透過 Proxy 代理操作物件,就可以攔截對物件的所有操作,不只讀取或寫入物件屬性,還包括迭代物件、使用 in
運算子、透過關鍵字 new
建立物件實體等等。
Syntax:
const proxy = new Proxy(target, handler);
const target = {
message1: "hello",
message2: "everyone"
};
obj.propertyName
取值時,會觸發 get trap,還有對物件屬性做賦值時,會觸發 set trap。const handler = {
get(target, property, receiver) {
console.log(`有人用了物件的 getter 讀取 ${property}`);
return Reflect.get(...arguments);
},
set(target, property, value) {
console.log(`有人將物件的 ${property} 賦值為 ${value}`);
target[property] = value;
return true;
},
};
將 target 和 handler 傳進去,會建立一個代理 target 的 Proxy 物件。
const proxy = new Proxy(target, handler);
proxy.newItem = "新屬性";
//會印出: 有人將物件的 newItem 賦值為 新屬性
myMessage = proxy.message1
//透過 proxy 讀取 target 物件
//會印出:有人用了物件的 getter 讀取 message1
myMessage = target.message1
//直接讀取 target 物件
//不會觸發 handler
Proxy 對 get 和 set 的 trap 是針對物件下所有屬性的讀取和寫入,所以很全面,以 array 資料來會更有感:
//續用上面的 handler
const todoArr = ["eat", "sleep", "coding"];
const arrProxy = new Proxy(todoArr, handler);
arrProxy.push("rolling");
//依序印出:
//有人用了物件的 getter 讀取 push
//有人用了物件的 getter 讀取 length
//有人將物件的 3 賦值為 rolling
//有人將物件的 length 賦值為 4
當我們用 push
新增了一個項目到陣列,其實不只是對屬性為 3
的項目進行賦值,同時還更動了陣列底下的 length
,透過 Proxy 操作就全部都會觸發自訂的 handler。
註:如果對陣列 method 和 length
屬性的關係感覺疑惑,推薦看這篇 Day 30 咩色用得好 - 所以我說...陣列到底是什麼? 還有整個系列。
通過上面的範例,可以了解到 Proxy 的優勢,以及 getter, setter 的不足之處(應該說 Proxy 大勝阿~)。
getter+setter | Proxy | |
---|---|---|
適用型別 | 物件 | 物件 |
可以攔截到的操作 | 單一屬性的讀取跟寫入 | 全部屬性的讀取跟寫入,甚至其他對物件的操作 |
能攔截到「新增」或「刪除」屬性 | 不行 | 可以 |
今天先簡單說明 reactive
和 ref
的響應原理,明天再來細看並比較兩者的特性。
reactive()
是用 Proxy 來實作,主要是拿來處理物件型別資料的響應性。
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key) //簡單來說,是用來紀錄相依邏輯和資料
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key) //簡單來說,是用來執行相依邏輯和更新資料
}
})
}
將物件型別資料傳入 reactive()
,會回傳一個相對應的 Proxy 物件
reactive()
傳入相同的物件或已經存在的 Proxy,會回傳相同的 Proxy,這個是 Vue 幫忙做的,以免同個物件有多個代碼,在原生的情況下並不會相等。const rawObject = {};
const proxy = reactive(rawObject);
console.log(reactive(rawObject) === proxy); // true
console.log(reactive(proxy) === proxy); // true
ref()
是用 getter 和 setter 來實作,主要是拿來處理基本型別資料的響應性,但是他也可以接受物件型別。(有沒有很迷惑的感覺XD)
等等,前面不是說 getter 和 setter 沒辦法用在基本型別上嗎?
沒錯,所以 ref 不是直接讓基本型別變成響應性(原生 Javascript 本身就做不到),而是透過把基本型別放到物件的 value
屬性下,來追蹤資料的變更。
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value') //簡單來說,是用來紀錄相依邏輯和資料
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value') //簡單來說,是用來執行相依邏輯和更新資料
}
}
return refObject
}
value
屬性下,並回傳 RefImpl 物件value
屬性定義 getter 跟 setter,去攔截對 value
的讀取跟寫入。ref()
創造回傳的 reference,來維持響應性,而不是資料(value)本身,所以基本型別才能透過 ref()
來達成響應const luckyNumber = ref(7);
console.log(`luckyNumber:`, luckyNumber);
所以 ref 可以接收物件型別,為什麼我們還需要 reactive?
將物件型別傳入 ref ,實際上,響應性還是靠 reactive 達成的。
ref
會利用 reactive
轉換此物件的 .value
值//ref 遇到物件型別,會用 reactive 去轉換再裝到 value 屬性下
const innerObj = { name: "innerObj" };
const refInnerObj = ref(innerObj);
console.log(`refInnerObj`, refInnerObj);
murmur:
其實本來沒有計畫分上下篇的,想在同一篇內一起說明兩個 API 的特性和限制,但是字數實在太多拉QQ
今天就先講到這裡,明天再繼續認識 ref()
和 reactive()
(遠目